package br.com.pontoclass.primes;

import java.io.IOException;
import java.math.BigInteger;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

import br.com.pontoclass.google.GoogleAuthorizer;
import br.com.pontoclass.google.exception.CellFeedNotCreatedException;
import br.com.pontoclass.google.exception.GoogleServiceException;
import br.com.pontoclass.google.exception.SpreadSheetNotCreatedException;
import br.com.pontoclass.patterns.StoppablePrimeNumberListener;
import br.com.pontoclass.patterns.StoppableSimpleNumberListener;
import br.com.pontoclass.primes.util.MathLogicUtil;

import com.google.gdata.client.GoogleService;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.spreadsheet.CellEntry;
import com.google.gdata.data.spreadsheet.CellFeed;
import com.google.gdata.data.spreadsheet.SpreadsheetEntry;
import com.google.gdata.data.spreadsheet.WorksheetEntry;
import com.google.gdata.data.spreadsheet.WorksheetFeed;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ServiceException;
import com.google.gdata.util.ServiceForbiddenException;


public class GoogleDriveXLSPrimeNumberListener implements StoppablePrimeNumberListener, StoppableSimpleNumberListener {

    private static Map<Integer, CellFeed> cellFeeds;
    private int base;
    private int line = 0;
    private int column = 0;
    private int columnAux = 0;
    private boolean stopped;
    private List<CellEntry> cells;
    private boolean persisted;
    private GoogleAuthorizer auth;
    private String spreadSheet;
    private String spreadsheetGoogleURL;
    private String spreadsheetGoogleURLForCreation;
    private int colFactor;
    private int worksheetNo = 1;
    private static final Logger LOGGER = Logger.getLogger(GoogleDriveXLSPrimeNumberListener.class.getName());
    private GoogleService service;
    private SpreadsheetEntry entry;
    private List<Object[]> its = Collections.synchronizedList(new ArrayList<Object[]>());
    private List<Thread> threads = Collections.synchronizedList(new ArrayList<Thread>());
    private @Autowired @Qualifier("maxWorksheets") Integer maxWorksheets;
            
    private GoogleDriveXLSPrimeNumberListener(SpreadsheetEntry entry, int base, GoogleService service, GoogleAuthorizer auth, String spreadSheet, String spreadsheetGoogleURL, String spreadsheetGoogleURLForCreation) throws CellFeedNotCreatedException {
        this.entry = entry;
        this.base = base;
        this.service = service;
        this.auth = auth;
        this.spreadSheet = spreadSheet;
        this.spreadsheetGoogleURL = spreadsheetGoogleURL;
        this.spreadsheetGoogleURLForCreation = spreadsheetGoogleURLForCreation;
        int hpr = base*100;
        LOGGER.log(Level.INFO, String.format("Estimated ammount of cells per worksheet: %d", hpr));
        cells = Collections.synchronizedList(new ArrayList<CellEntry>(hpr));
        hpr = 1000000/(((20-base)/base)*hpr) + (1000000%(((20-base)/base)*hpr) > 0? 1: 0);
        LOGGER.log(Level.INFO, String.format("Estimated ammount of worksheets: %d", hpr));
        cellFeeds = Collections.synchronizedMap(new HashMap<Integer, CellFeed>(hpr));
    }
    
    public static GoogleDriveXLSPrimeNumberListener newInstance(GoogleAuthorizer auth, String spreadSheet, String spreadsheetGoogleURL, String spreadsheetGoogleURLForCreation, int base) throws AuthenticationException, SpreadSheetNotCreatedException, CellFeedNotCreatedException{
        GoogleService service = auth.doSpreadsheetAuth();
        return new GoogleDriveXLSPrimeNumberListener(getSpreadSheetEntry(service, spreadSheet, spreadsheetGoogleURL, spreadsheetGoogleURLForCreation, auth.getUserName(), auth.getApplicationId()), base, service, auth, spreadSheet, spreadsheetGoogleURL, spreadsheetGoogleURLForCreation);
    }
    
    public static GoogleDriveXLSPrimeNumberListener newInstance(GoogleAuthorizer auth, String spreadSheet, String spreadsheetGoogleURL, String spreadsheetGoogleURLForCreation) throws AuthenticationException, SpreadSheetNotCreatedException, CellFeedNotCreatedException{
        return newInstance(auth, spreadSheet, spreadsheetGoogleURL, spreadsheetGoogleURLForCreation, 10);
    }

    private static SpreadsheetEntry getSpreadSheetEntry(GoogleService service, String spreadSheet, String spreadsheetGoogleURL, String spreadsheetGoogleURLForCreation, String userName, String applicationId) throws SpreadSheetNotCreatedException {
        return GoogleSpreadsheetUtil.buildSpreadsheetEntry(service, spreadSheet, spreadsheetGoogleURL, spreadsheetGoogleURLForCreation, userName, applicationId);
    }

    private static CellFeed getOrBuildCellFeed(GoogleService service, SpreadsheetEntry entry, Integer worksheetNo) throws CellFeedNotCreatedException {
        if(cellFeeds.containsKey(worksheetNo)) {
            return cellFeeds.get(worksheetNo);
        }
        synchronized (cellFeeds) {
            try {
                URL worksheetFeedUrl = entry.getWorksheetFeedUrl();
                WorksheetFeed worksheetFeed = service.getFeed(worksheetFeedUrl, WorksheetFeed.class);
                List<WorksheetEntry> worksheetEntries = worksheetFeed.getEntries();
                WorksheetEntry worksheetEntry = null;
                if(worksheetEntries.size() < worksheetNo) {
                    int diff = worksheetNo - worksheetEntries.size();
                    while(--diff >= 0) {
                        worksheetEntry = new WorksheetEntry();
                        worksheetEntry.setTitle(new PlainTextConstruct(String.format("%s - Worksheet %d", entry.getTitle().getPlainText(), cellFeeds.size() + 1)));
                        worksheetEntry.setColCount(20);
                        worksheetEntry.setRowCount(100);
                        service.insert(worksheetFeedUrl, worksheetEntry);
                        URL cellFeedUrl = null;
                        do {
                            try{
                                cellFeedUrl = worksheetEntry.getCellFeedUrl();
                            } catch(NullPointerException e) {
                                cellFeedUrl = worksheetEntry.getCellFeedUrl();
                            }
                        } while (cellFeedUrl == null);
                        CellFeed c = service.getFeed(cellFeedUrl, CellFeed.class);
                        cellFeeds.put(cellFeeds.size()+1, c);
                    }
                } else {
                    worksheetEntry = worksheetEntries.get(worksheetNo-1);
                    URL cellFeedUrl = worksheetEntry.getCellFeedUrl();
                    CellFeed c = service.getFeed(cellFeedUrl, CellFeed.class);
                    cellFeeds.put(cellFeeds.size() + 1, c);
                }
                if(worksheetEntry == null) {
                    throw new GoogleServiceException(String.format("Something went wrong with worksheet %d creation!", worksheetNo));
                }
                if(worksheetEntry.getTitle().getPlainText().startsWith("Sheet")) {
                    worksheetEntry.setTitle(new PlainTextConstruct(String.format("%s - Worksheet %d", entry.getTitle().getPlainText(), worksheetNo)));
                    worksheetEntry.setColCount(20);
                    worksheetEntry.setRowCount(100);
                    worksheetEntry.update();
                }
                return cellFeeds.get(worksheetNo);
            } catch (IOException | ServiceException e) {
                throw new CellFeedNotCreatedException(e);
            }
        }
    }

    @Override
    public void onAddPrime(BigInteger prime) {
        if(maxWorksheets < 1 || (((base*100)*((20-base)/base)*maxWorksheets)+1) >= MathLogicUtil.convertFrom(new BigInteger(Integer.toString(base)), prime).longValue()) {
            LOGGER.log(Level.INFO, String.format("Google, Prime: %s", prime.toString()));
            calculateNextCell();
            addCellEntry(new CellEntry(this.line, this.column, String.format("p:%s",prime.toString())));
            waitIfTooExpensiveMemory();
        } else {
            LOGGER.log(Level.INFO, String.format("Google, Prime: %s | NOT PROCESSED.", prime.toString()));
        }
    }
    
    @Override
    public void onAddSimple(BigInteger simple) {
        if(maxWorksheets < 1 || (((base*100)*((20-base)/base)*maxWorksheets)+1) >= MathLogicUtil.convertFrom(new BigInteger(Integer.toString(base)), simple).longValue()) {
            LOGGER.log(Level.INFO, String.format("Google, Simple: %s", simple.toString()));
            calculateNextCell();
            addCellEntry(new CellEntry(this.line, this.column,  String.format("s:%s",simple.toString())));
            waitIfTooExpensiveMemory();
        } else {
            LOGGER.log(Level.INFO, String.format("Google, Simple: %s | NOT PROCESSED", simple.toString()));
        }
    }

    private void addCellEntry(CellEntry cellEntry) {
        synchronized (cells) {
            cells.add(cellEntry);
            // 100 is actualy the default number of lines google gives us for each worksheet (I'm multiplying it by the base number so that I know how much has to be written)
            if(cells.size() == base*100) {
                List<CellEntry> it = cells.subList(0, (base*100)),
                newList = new ArrayList<>(it);
                its.add(new Object[]{worksheetNo, newList});
                cells.removeAll(it);
                line = 0;
                // 20 is the default number of cels google gives us for each line...
                if(((20-base)/base) <= (colFactor+1)) {
                    colFactor = 0;
                    column = columnAux = 0;
                    ++worksheetNo;
                    fetchNewWorksheet();
                } else {
                    colFactor++;
                }
            }
        }
    }

    private void waitIfTooExpensiveMemory() {
        Runtime rt = Runtime.getRuntime();
        
        long allocated = (100*rt.totalMemory())/rt.maxMemory();
        long used = (100*(rt.maxMemory() - rt.freeMemory())/rt.maxMemory());
        logMemoryUsage(rt);
        if(allocated == 100 && used > 85) {
            do {
                Thread.yield();
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    LOGGER.log(Level.WARNING, "It looks there was some problem with the thread. =(", e);
                }
                allocated = (100*rt.totalMemory())/rt.maxMemory();
                used = (100*(rt.maxMemory() - rt.freeMemory())/rt.maxMemory());
                logMemoryUsage(rt);
            } while(allocated == 100 && used > 85);
        }
    }

    private void logMemoryUsage(Runtime rt) {
        long maxMemory = rt.maxMemory();
        long allocatedMemory = rt.totalMemory();
        long freeMemory = rt.freeMemory();
        LOGGER.log(Level.INFO, String.format("Allocated Mem.: %d mb, percentage: %d%%", allocatedMemory / 1024, (100*allocatedMemory)/maxMemory));
        LOGGER.log(Level.INFO, String.format("Free Mem.: %d mb, percentage: %d%%", freeMemory / 1024, (100*freeMemory)/maxMemory));
        LOGGER.log(Level.INFO, String.format("Max Mem.: %d mb, percentage: %d%%", maxMemory / 1024, (100*maxMemory)/maxMemory));
        LOGGER.log(Level.INFO, String.format("Total Used Mem.: %d mb, percentage: %d%%", (maxMemory - freeMemory) / 1024, (100*(maxMemory - freeMemory)/maxMemory)));
    }

    private void calculateNextCell() {
        columnAux = ((this.columnAux%base)+1);
        this.column = (colFactor*(base+1)) + columnAux;
        if(this.columnAux == 1) {
            this.line++;
        }
        if(line < 1) {
            line = 1;
        }
        if(column < 1) {
            column = 1;
        }
    }

    @Override
    public void stop() {
        this.stopped = true;
    }

    private CellFeed getCellFeed(GoogleService service, SpreadsheetEntry entry, int wn) {
        CellFeed cf;
        try {
            try {
                cf = getOrBuildCellFeed(service, GoogleDriveXLSPrimeNumberListener.this.entry, wn);
            } catch(CellFeedNotCreatedException e1) {
                try {
                    GoogleDriveXLSPrimeNumberListener.this.entry = getSpreadSheetEntry(service, spreadSheet, spreadsheetGoogleURL, spreadsheetGoogleURLForCreation, auth.getUserName(), auth.getApplicationId());
                } catch(SpreadSheetNotCreatedException e2) {
                    GoogleDriveXLSPrimeNumberListener.this.entry = getSpreadSheetEntry(auth.doSpreadsheetAuth(), spreadSheet, spreadsheetGoogleURL, spreadsheetGoogleURLForCreation, auth.getUserName(), auth.getApplicationId());
                }
                cf = getOrBuildCellFeed(service, GoogleDriveXLSPrimeNumberListener.this.entry, worksheetNo);
            }
        } catch(AuthenticationException | CellFeedNotCreatedException | SpreadSheetNotCreatedException e) {
            throw new GoogleServiceException(e);
        }
        return cf;
    }
    
    private void fetchNewWorksheet() {
        synchronized (entry) {
            try {
                try {
                    getOrBuildCellFeed(service, this.entry, worksheetNo);
                } catch(CellFeedNotCreatedException e) {
                    try {
                        this.entry = getSpreadSheetEntry(service, spreadSheet, spreadsheetGoogleURL, spreadsheetGoogleURLForCreation, auth.getUserName(), auth.getApplicationId());
                    } catch(SpreadSheetNotCreatedException e1) {
                        this.entry = getSpreadSheetEntry(auth.doSpreadsheetAuth(), spreadSheet, spreadsheetGoogleURL, spreadsheetGoogleURLForCreation, auth.getUserName(), auth.getApplicationId());
                    }
                    getOrBuildCellFeed(service, this.entry, worksheetNo);
                }
            } catch(SpreadSheetNotCreatedException | CellFeedNotCreatedException | AuthenticationException e2) {
                throw new GoogleServiceException(e2);
            }
        }
    }
    
    private void tryInsert(final CellFeed cf, final CellEntry entry) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (cf) {
                    boolean flag = true;
                    do {
                        try {
                            cf.insert(entry);
                            flag = false;
                        } catch(ServiceForbiddenException e) {
                            LOGGER.log(Level.WARNING, "It looks google is not enjoing the game...", e);
                        } catch(Exception e) {
                            LOGGER.log(Level.WARNING, "It looks something went wrong.", e);
                        }
                    } while (flag);
                }
            }
        });
        t.setPriority(Thread.MAX_PRIORITY);
        t.start();
    }

    @Override
    public void run() {
        while((!stopped) || (!persisted)) {
            if(!stopped) {
                synchronized (its) {
                    if(!its.isEmpty()) {
                        Object[] arr;
                        Integer wn = Integer.valueOf((arr=its.remove(0))[0].toString());
                        @SuppressWarnings("unchecked") List<CellEntry> it = List.class.cast(arr[1]);
                        threads.add(new Thread(new HundredsOfEntriesRunnable(wn, it, service, entry)));
                        threads.get(threads.size()-1).setPriority(Thread.MAX_PRIORITY-1);
                        threads.get(threads.size()-1).start();
                    }
                }
            } else {
                synchronized (threads) {
                    threads.add(new Thread(new FinalSpreadsheetFillerRunnable()));
                    threads.get(threads.size()-1).setPriority(Thread.MAX_PRIORITY-2);
                    threads.get(threads.size()-1).start();
                }
                persisted = true;
            }
            List<Thread> removed = new ArrayList<>();
            synchronized (threads) {
                for(Thread thread: threads) {
                    if(!thread.isAlive()) {
                        removed.add(thread);
                    }
                }
                for(Thread thread: removed) {
                    threads.remove(thread);
                }
            }
        }
    }
    
    
    private class HundredsOfEntriesRunnable implements Runnable {

        private List<CellEntry> entries;
        private Integer worksheetNo;
        private SpreadsheetEntry entry;
        private GoogleService service;

        public HundredsOfEntriesRunnable(Integer worksheetNo, List<CellEntry> entries, GoogleService service, SpreadsheetEntry entry) {
            this.worksheetNo = worksheetNo;
            this.entries = entries;
            this.service = service;
            this.entry = entry;
            
        }
        
        @Override
        public void run() {
            synchronized (worksheetNo) {
                final CellFeed cf = getCellFeed(service, entry, worksheetNo);
                while(!entries.isEmpty()) {
                    CellEntry entry = entries.remove(0);
                    tryInsert(cf, entry);
                    entry = null;
                    System.gc();
                    Thread.yield();
                }
            }
            this.worksheetNo = null;
            this.entries = null;
            this.service = null;
            this.entry = null;
        }
    }
    
    public class FinalSpreadsheetFillerRunnable implements Runnable {
        @Override
        public void run() {
            while(!cells.isEmpty()) {
                CellEntry entry = cells.remove(0);
                final CellFeed cf = getCellFeed(service, GoogleDriveXLSPrimeNumberListener.this.entry, worksheetNo);
                tryInsert(cf, entry);
                entry = null;
                System.gc();
                Thread.yield();
            }
            cells = null;
        }
    }
}